#!/usr/bin/python3

import os
import os.path
import sys
import glob
import datetime
import fnmatch
import pwd, grp
import json
import subprocess
from subprocess import call
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, Boolean
from sqlalchemy.pool import NullPool
from contextlib import contextmanager
from pathlib import Path

os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__))))
sys.path.append("../modules")

from metro_support import lockFile, countFile, CommandRunner, MetroSetup

# these variables are used for building:

builds = ( )
arches = ( )
subarches = ( )
index_ignore_builds = ()
index_ignore_subarches = ()
# these variables, if defined, are used for repo management (cleaning, etc.):

all_builds = ( )
all_arches = ( )
all_subarches = ( )
stale_days = 3
max_failcount = 3
keep_num = 2

def map_build(build, subarch, full, full_date):
	return "full"
cfgfile = os.path.join(os.path.expanduser("~"),".buildbot")
if os.path.exists(cfgfile):
	exec(open(cfgfile, "rb").read())
else:
	print("""
Create a ~/.buildbot file that contains something like this (python syntax):

builds = (
"funtoo-experimental",
"funtoo-current",
"funtoo-current-hardened",
)

arches = (
"x86-32bit",
"x86-64bit",
"sparc-64bit",
"pure64"
)

# all subarches that are built here:
subarches = ( 
"atom_32",
"atom_64",
"corei7",
"corei7-pure64",
"generic_32", 
"i686", 
"athlon-xp",
"pentium4",
"core2_32",
"amd64-k8_32",
"amd64-k8",
"amd64-k10",
"core2_64",
"generic_64",
"generic_64-pure64",
)

# optional - how many days old it takes for a stage build to be considered "stale":
# default = 3
stale_days = 3

# optional - how many consecutive failed builds of a subarch before it is pulled from
# build rotation. default = 3
max_failcount = 3

# optional - number of builds to keep, default = 3
keep_num = 3

# optional - all subarches that are built + uploaded here:
all_subarches = subarches + (
"amd64-piledriver",
"amd64-steamroller"
)

# optional:
def map_build(build, subarch, full, full_date):
# arguments refer to last build...
if full == True:
	buildtype = "freshen"
else:
	buildtype = "full"
if subarch in [ "corei7", "corei7-pure64", "generic_64",  "generic_64-pure64" ]:
	buildtype = buildtype + "+openvz"
return buildtype
""")
	sys.exit(1)

if len(all_builds) == 0:
	# if the all_variables are not defined, inherit values from the non-all vars:
	all_builds = builds
	all_arches = arches
	all_subarches = subarches

class Database(object):
	poolclass = NullPool
	autoflush = True
	pool_recycle = -1
	cfg_section = "database"
	cfg_setting = "connection"
	echo = False
	orm_objects = {}

	def orm_register(self, *args):
		"""
		This method takes any number of classes as arguments and makes these classes available as properties
		of self. So self.orm_register(Foo) would make the class Foo accessible at self.Foo.

		:param args: any number of classes.
		:return: None
		"""
		for classdef in args:
			classname = classdef.__name__
			self.orm_objects[classname] = classdef

	def __getattr__(self, item):
		return self.orm_objects[item]

	def __dir__(self):
		return super().__dir__() + [str(k) for k in self.orm_objects.keys()]

	def initialize(self):
		"""Override this class to create tables, etc."""
		pass

	def __init__(self, **kwargs):
		self.engine = create_engine("sqlite:///cleaner.db", poolclass=self.poolclass, pool_recycle=self.pool_recycle, echo=self.echo)
		self.Base = declarative_base(bind=self.engine)
		self.session_factory = sessionmaker(bind=self.engine, autoflush=self.autoflush)
		self.Session = scoped_session(self.session_factory)
		self.initialize()

		self.Base.metadata.create_all(self.engine)

	@property
	@contextmanager
	def session(self):
		session = self.Session()
		try:
			yield session
		except:
			session.rollback()
			raise
		else:
			session.commit()

	@property
	@contextmanager
	def session_no_flush(self):
		self.Session.configure(autoflush=False)
		session = self.Session()
		try:
			yield session
		except:
			session.rollback()
			raise
		else:
			session.commit()

	def get_session(self):
		return self.session

class MetroRepositoryDatabase(Database):

	def initialize(self):

		class SubArch(self.Base):

			"""
			This class represents a subarch of a particular release. It also includes some date-specific information.
			"""

			__tablename__ = "subarches"

			id = Column(Integer, primary_key = True)

			path = Column(String, index=True)
			release = Column(String, index=True)
			arch = Column(String, index=True)
			subarch = Column(String, index=True)
			failcount = Column(Integer, index=True)

			do_build = Column(Boolean, index=True)

			# latest_full_build is destined to replace the de-normalized crap below it eventually:

			latest_full_build_id = Column(Integer, ForeignKey("build_dirs.id"))
			latest_full_build = relationship("BuildDir", foreign_keys=[latest_full_build_id])
			# de-normalized crap, status info about the subarch:
			date = Column(DateTime, index=True)
			date_str = Column(String, index=True)
			full_date = Column(DateTime, index=True)
			full_date_str = Column(String, index=True)


		class BuildDir(self.Base):

			"""
			This class represents a specific build directory for a subarch, like "2019-10-15".
			"""

			__tablename__ = "build_dirs"

			id = Column(Integer, primary_key = True)
			subarch_id = Column(Integer, ForeignKey('subarches.id'))
			date = Column(DateTime, index=True)
			path = Column(String, index=True)
			release = Column(String, index=True)
			arch = Column(String, index=True)
			subarch_name = Column(String, index=True)
			date_str = Column(String, index=True)
			complete = Column(Boolean, index=True)      # complete currently means: stage3 exists.
			full = Column(Boolean, index=True)          # full build means: stage 1, 2, 3
			subarch = relationship(SubArch, backref=backref("build_dirs", order_by="BuildDir.date_str"), primaryjoin="SubArch.id == BuildDir.subarch_id")

		class Artifact(self.Base):

			"""
			This class represents a specific artifact of a build directory, such as a stage3, lxd image, or gnome
			image.
			"""

			__tablename__ = "artifacts"

			id = Column(Integer, primary_key=True)
			build_dir_id = Column(Integer, ForeignKey('build_dirs.id'))
			flavor = Column(String)
			path = Column(String, index=True)
			build_dir = relationship(BuildDir, backref=backref("artifacts", order_by="Artifact.flavor"))
			date_str = Column(String)

		class Snapshot(self.Base):

			__tablename__ = "snapshots"

			id = Column(Integer, primary_key = True)
			path = Column(String, index=True)
			release = Column(String, index=True)

		self.orm_register(BuildDir, Snapshot, SubArch, Artifact)

setup = MetroSetup()
settings = setup.getSettings()
initial_path = settings["path/mirror"]

def find_file_compressed_or_not(glob_pattern):
	found_match = None
	for match in glob.glob(glob_pattern):
		for ending in [ ".tar.bz2", "tar.gz", "tar.xz", ".tar" ]:
			if match.endswith(ending):
				found_match = match
				break
		if found_match:
			break
	return found_match

if os.path.exists("cleaner.db"):
	os.unlink("cleaner.db")
db = MetroRepositoryDatabase()
for release in all_builds:
	if not os.path.exists("%s/%s" % (initial_path, release)):
		continue
	snapdir = "%s/%s/snapshots" % ( initial_path, release )
	if os.path.isdir(snapdir) and not os.path.islink(snapdir):
		for match in glob.glob("%s/portage-*.tar.xz" % snapdir):
			basename = os.path.basename(match)
			if basename == "portage-current.tar.xz":
				continue
			sna = db.Snapshot()
			sna.path = match
			sna.release = release
			with db.get_session() as session:
				session.add(sna)
				session.commit()
	with db.get_session() as session:
		for arch in all_arches:
			funky_path = "%s/%s/%s" % ( initial_path, release, arch )
			if not os.path.exists(funky_path):
				print("Path not found: %s" % funky_path)
				continue
			for subarch in all_subarches:
				path = "%s/%s/%s/%s" % (initial_path, release, arch, subarch)
				if not os.path.exists(path):
					continue

				most_recent_build = None
				most_recent_complete_build = None
				most_recent_complete_build_id = None

				failpath = path + "/.control/.failcount"
				if not os.path.exists(failpath):
					failcount = 0
				else:
					fc = countFile(failpath)
					failcount = fc.count

				# LET'S CREATE A SEMI-POPULATED SUBARCH. WE WILL FILL OUT DATE FIELDS LATER:

				sa = db.SubArch()
				sa.release = release
				sa.arch = arch
				sa.subarch = subarch
				sa.path = path
				session.add(sa)
				session.flush()

				for dirname in os.listdir(path):
					if dirname == ".control":
						continue
					ipath = "%s/%s" % ( path, dirname )
					if not os.path.isdir(ipath):
						continue
					complete = False
					# artifacts: map 'extra' name like 'gnome' build to full path of artifact:
					artifacts = {}

					# CREATE NEW BUILD DIR DB ENTRY:

					bdir = db.BuildDir()
					spath = ipath + "/status"
					if os.path.exists(spath):
						with open(spath, "r") as sfile:
							sdata = sfile.read().strip()
							if sdata == "ok":
								complete = True
							# grab mtime of status file:
							bdir.date = datetime.datetime.utcfromtimestamp(os.path.getmtime(spath))
							# use that to generate date string:
							bdir.date_str = bdir.date.strftime("%Y-%m-%d")
					else:
						# build is in progress, so skip:
						continue
					# datestamped build dir -- let's try to parse it:
					try:
						bdir.date = datetime.datetime.strptime(dirname,"%Y-%m-%d")
						bdir.date_str = dirname
					except ValueError:
						# didn't work? Use old date on purpose to force cleaning:
						bdir.date = datetime.datetime(1980,1,1)
						bdir.date_str = "1980-01-01"
					artifacts["stage3"] = find_file_compressed_or_not("%s/stage3*.tar*" % ipath)
					if artifacts["stage3"]is not None:
						# if we just have a stage3, we consider it "complete" for our purposes, for now
						complete = True
					artifacts["openvz"] = find_file_compressed_or_not("%s/funtoo-*-openvz-*.tar*" % ipath)
					artifacts["lxd"] = find_file_compressed_or_not("%s/lxd-*.tar*" % ipath)
					artifacts["gnome"] = find_file_compressed_or_not("%s/gnome-*.tar*" % ipath)

					bdir.path = ipath
					bdir.release = release
					bdir.arch = arch
					bdir.subarch_name = subarch
					bdir.complete = complete
					bdir.subarch_id = sa.id

					if complete:
						for match in glob.glob("%s/stage1*.tar.*" % ipath):
							bdir.full = True
							break
					session.add(bdir)
					session.flush()

					# NOW: CREATE ARTIFACTS, LINK TO BUILD DIR

					for artifact_keyword, artifact_path in artifacts.items():
						if artifact_path is None:
							continue
						art = db.Artifact()
						art.build_dir_id = bdir.id
						art.flavor = artifact_keyword
						art.path = artifact_path
						art.date_str = bdir.date_str
						session.add(art)
						session.flush()

					if complete and (most_recent_build is None or most_recent_build.date < bdir.date):
						most_recent_build = bdir
						if bdir.complete:
							most_recent_complete_build = bdir
							most_recent_complete_build_id = bdir.id

				# NOW, UPDATE SUBARCH OBJECT TO INCLUDE DATE INFORMATION:

				sa = session.query(db.SubArch).filter(db.SubArch.id == sa.id).first()
				sa.date = most_recent_build.date if most_recent_build else None
				sa.date_str = most_recent_build.date_str if most_recent_build else None
				sa.full_date = most_recent_complete_build.date if most_recent_complete_build else None
				sa.full_date_str = most_recent_complete_build.date_str if most_recent_complete_build else None
				sa.latest_full_build_id = most_recent_complete_build_id
				sa.failcount = failcount

				# is subarch in our list of things to build, or just to maintain (ie. clean, for main repo)?
				if release in builds and arch in arches and subarch in subarches:
					sa.do_build = True
				else:
					sa.do_build = False
				session.add(sa)
				session.flush()

			session.commit()

now = datetime.datetime.now()


def find_build(q, min_age=stale_days, max_age=None):
	for x in q:
		if min_age is not None:
			if x.date is not None and now - x.date < datetime.timedelta(days=min_age):
				continue	
		if max_age is not None:
			if x.date is None:
				continue
			elif now - x.date > datetime.timedelta(days=max_age):
				continue	
		# skip it if it is currently being built....
		tsfilepath = x.path+"/.control/.multi_progress"
		if os.path.exists(tsfilepath):
			tsfile = lockFile(tsfilepath)
			# ensure lockFile is not stale. The exists() method will clean it up automatically if it is:
			if tsfile.exists():
				sys.stderr.write("# Build at %s in progress, skipping...\n" % x.path)
				continue
		# output: build subarch was-last-build-full(had-a-stage-1)(boolean) date
		# TODO: this has changed to RELEASE, and buildbot needs to be updated to understand it.
		print("release=%s" % x.release)
		print("arch_desc=%s" % x.arch)
		print("subarch=%s" % x.subarch)
		print("fulldate=%s" % x.full_date_str)
		print("nextdate=%s" % datetime.datetime.strftime(datetime.datetime.now(), "%Y-%m-%d"))
		print("failcount=%s" % x.failcount)
		mb = map_build(x.release, x.subarch, x.full_date == x.date and x.date != None, x.full_date_str)
		# handle case where map_build returns "full+openvz":
		if type(mb) == str:
			mb = mb.split("+")
		# handle case where map_build returns ("full", "openvz"):
		print("target=%s" % mb[0])
		if len(mb) > 1:
			print("extras='%s'" % "+".join(mb[1:]))
		else:
			print("extras=''")
		sys.exit(0)


if sys.argv[1] == "clean":
	with db.get_session() as session:
		for release in all_builds:
			# first -- remove any more than keep_num complete builds, starting with oldest, of course:
			for arch in all_arches:
				for subarch in all_subarches:
					out = session.query(db.BuildDir).filter_by(release=release).filter_by(arch=arch)
					out = out.filter_by(subarch_name=subarch).filter_by(complete=True).order_by(db.BuildDir.date_str).all()
					for x in out[0:-keep_num]:
						print(("rm -rf %s" % x.path))
					for x in out[-keep_num:]:
						print(("# keeping %s" % x.path))
			# next, remove any more than keep_num snapshots:
			sna = session.query(db.Snapshot).filter_by(release=release).order_by(db.Snapshot.path).all()
			for x in sna[0:-keep_num]:
				print(("rm %s*" % x.path))
			for x in sna[-(keep_num-1):]:
				print(("# keeping %s" % x.path))
		# Now, look at incomplete builds. Clean them out when they're stale and not the most recent build.
		for x in session.query(db.BuildDir).filter_by(complete=False):
			# ignore non-stale builds for cleaning -- they could be in-progress...
			y = session.query(db.SubArch).filter_by(subarch=x.subarch_name).filter_by(arch=x.arch)
			y = y.filter_by(release=x.release).order_by(db.SubArch.date_str).first()
			if x.date is not None and now - x.date > datetime.timedelta(days=stale_days) and y.date != x.date:
				# don't zap most recent...
				print(("rm -rf %s* # not complete and stale, not most recent build" % x.path))
			else:
				print("# keeping %s" % x.path)
elif sys.argv[1] == "nextbuild":
	with db.get_session() as session:
		sa = session.query(db.SubArch)
		myfail = 0
		while myfail < max_failcount:
			# we start with failcount of zero, and prioritize not-yet-built stages:
			empties = sa.filter(db.SubArch.do_build == True).filter(db.SubArch.date == None).filter(db.SubArch.release.in_(builds)).filter_by(failcount=myfail)
			find_build(empties)
			# next, we look for builds with a failcount of "myfail+1", but haven't been built in at least stale_days * 2. 
			# This prevents them from being pushed out of the build rotation if we aren't building *every* stale stage (if we always have some builds queued at any given time.)
			if myfail + 1 < max_failcount:
				baddies = sa.filter(db.SubArch.do_build == True).filter(db.SubArch.release.in_(builds)).filter_by(failcount=myfail+1).order_by(db.SubArch.date)
				find_build(baddies,min_age=stale_days*2)
			# if we can't find one, we look for a failcount of zero, but prioritize by date:
			oldies = sa.filter(db.SubArch.do_build == True).filter(db.SubArch.date != None).filter(db.SubArch.release.in_(builds)).filter_by(failcount=myfail).order_by(db.SubArch.date)
			find_build(oldies)
			myfail += 1
			# if no builds are found, we increment failcount, and try again:
		# THIS PART IS DISABLED. IF A BUILD IS BEYOND ITS FAILCOUNT, IT IS OUT OF ROTATION UNTIL SOMEONE MANUALLY INTERVENES:
		# if our loop didn't present a build, we ignore date, look for all builds with failcount >= our max, and then order by failcount, and pick a build:
		#baddies = sa.filter(SubArch.do_build == True).filter(SubArch.build.in_(builds)).filter(SubArch.failcount >= failcount).order_by(SubArch.failcount.asc())
		#find_build(baddies)
		sys.exit(1)
elif sys.argv[1] == "empties":
	with db.get_session() as session:
		empties = session.query(db.SubArch).filter(db.SubArch.do_build is True).filter(db.SubArch.date is None).filter(db.SubArch.release.in_(builds))
		for x in empties:
			print(x.path)
elif sys.argv[1] == "fails":
	with db.get_session() as session:
		fails = session.query(db.SubArch).filter(db.SubArch.failcount > 0).order_by(db.SubArch.failcount.desc())
		goods = session.query(db.SubArch).filter(db.SubArch.failcount == 0)
		for x in list(fails) + list(goods):
			# some stuff is not cached in the subarch db entry, so we need to grab the associated most recent build directory entry for the subarch:
			bdir = x.latest_full_build
			art_list = [art.flavor for art in bdir.artifacts] if bdir else []
			print(str(x.failcount).rjust(4), "None".rjust(12) if not x.date else datetime.datetime.strftime(x.date, "%Y-%m-%d").rjust(12), x.path, "(%s)" % ",".join(art_list))
elif sys.argv[1] == "index.xml":
	from lxml import etree
	"""
	<subarches>
		<subarch name="amd64-bulldozer" builds="1,2">
			<build id="1" variant="stage3" release="1.4-release-std" path="/1.4-release-std/blah/blah" latest="2018-12-31"/>
			<build id="2" variant="lxd" release="1.4-release-std" path="/1.4-release-std/blah/blah" latest="2018-12-31"/>
		</subarch>
	</subarches>
	"""
	outxml = etree.Element("subarches")
	subarch_builds = {}
	with db.get_session() as session:
		numfails = 0
		for subarch in session.query(db.SubArch).filter(db.SubArch.release.notin_(index_ignore_builds)):
			subarch_xml = etree.Element("subarch")
			subarch_xml.attrib["name"] = subarch.subarch
			count: int = 1
			info_json_path: str = None
			if subarch.latest_full_build is None:
				continue
			for artifact in subarch.latest_full_build.artifacts:
				build_xml = etree.Element("build")
				build_xml.attrib["id"] = str(count)
				build_xml.attrib["variant"] = artifact.flavor
				build_xml.attrib["release"] = subarch.release
				build_xml.attrib["path"] = subarch.path[len(initial_path):]
				build_xml.attrib["latest"] = "None" if not artifact.date_str else artifact.date_str
				build_xml.attrib["download"] = artifact.path[len(initial_path):]
				build_xml.attrib["size"] = subprocess.getoutput("/usr/bin/du -h "+artifact.path).strip().split()[0] + "B"
				if not info_json_path:
					bifn = os.path.join(subarch.path, artifact.date_str, "build-info.json")
					if os.path.exists(bifn):
						info_json_path = bifn
				subarch_xml.append(build_xml)
				count += 1
			subarch_xml.attrib["builds"] = ",".join(map(str, range(1, count)))

			# We found extracted CFLAGS, etc from most recent build -- grab this info and add it to our XML:
			if info_json_path:
				with open(info_json_path,"r") as bif:
					bif_json = json.loads(bif.read())
					for x in bif_json:
						subarch_xml.attrib[x] = bif_json[x]
			outxml.append(subarch_xml)
	outf = open(os.path.join(initial_path, "index.xml"), "wb")
	outf.write(etree.tostring(outxml, encoding="UTF-8", pretty_print=True, xml_declaration=True))
elif sys.argv[1] == "zap":
	with db.get_session() as session:
		for x in session.query(db.SubArch).filter(db.SubArch.do_build == True).filter(db.SubArch.failcount > 0):
			failpath = "%s/.control/.failcount" % x.path
			if os.path.exists(failpath):
				print("Removing %s..." % failpath)
				os.unlink(failpath)
elif sys.argv[1] == "compress":
	# this performs a global compression scan on the entire build repository, and will
	# compress files even if not built locally. This is so you can do compression on a
	# fast box.
	if len(sys.argv) != 3:
		print("Please specify path to scan as second argument.")
		sys.exit(1)
	
	def get_all_cme_files(path):
		for entry in os.scandir(path):
			if entry.is_file():
				if entry.name.endswith(".tar.cme"):
					yield entry
			elif entry.is_dir():
				yield from get_all_cme_files(entry.path)

	for entry in get_all_cme_files(sys.argv[2]):
		# we use .cme to indicate "compress me". It's a zero-length file. Indicates
		# tarball is ready to be compressed (done being created, to avoid race condition
		# of compressing 'in-progress' tarballs.) We unlink the .cme file after we're
		# done compressing. And we also exclude them from mirrors.
		tarfile = entry.path[:-4]
		if os.path.exists(tarfile):
			if os.path.exists(entry.path+".run"):
				print("Compression in progress, skipping...")
				continue
			comp = settings["target/compression"]
			if comp in [ "bz2", "xz", "gz" ]:
				print("Compressing file %s..." % tarfile)
				Path(entry.path+".run").touch()
				if comp == "bz2":
					call("PATH=/bin/:/usr/bin bzip2 -9 " + tarfile, shell=True)
				elif comp == "xz":
					call("PATH=/bin/:/usr/bin xz -9e --threads=0 " + tarfile, shell=True)
				elif comp == "gz":
					call("PATH=/bin/:/usr/bin gzip -9 " + tarfile, shell=True)
				try:
					os.unlink(entry.path+".run")
				except:
					pass
			# remove .cme file, and set p to the new compressed file.
			os.unlink(entry.path)
		else:
			print(tarfile, "doesn't exist, can't compress.")

elif sys.argv[1] == "digestgen":
	owner = settings["path/mirror/owner"]
	group = settings["path/mirror/group"]
	uid = pwd.getpwnam(owner)[2]
	gid = grp.getgrnam(group)[2] 
	print("Generating hashes...")
	links = []

	def get_all_relevant_files(path):
		for entry in os.scandir(path):
			if entry.is_file():
				ending = entry.name.split('.')
				if len(ending) and ending[-1] in [ "gz", "bz2", "xz" ]:
					if len(ending) >= 2 and ending[-2] == "tar":
						yield entry
			elif entry.is_dir():
				yield from get_all_relevant_files(entry.path)

	with db.get_session() as session:
		# only generate digests for the subarches of our local build system:
		all_subarches = session.query(db.SubArch)
		for subarch in all_subarches:
			print("Processing %s..." % subarch.path)
			for entry in get_all_relevant_files(subarch.path):
				p = entry.path
				if os.path.islink(p):
					links.append(p)
					continue
				print(p)
				if not os.path.exists(p+".hash.txt"):
					sys.stdout.write('h')
					sys.stdout.flush()
					call("PATH=/bin/:/usr/bin echo sha256 $(sha256sum %s | cut -f1 -d' ') > %s.hash.txt" % ( p, p ), shell=True)
				os.lchown(p+".hash.txt",uid,gid)
				if not os.path.exists(p+".gpg"):
					print(p+".gpg")
					sys.stdout.flush()
					call("PATH=/bin/:/usr/bin gpg --batch --homedir /root/.gnupg --use-agent --output %s.gpg --detach-sig %s" % (p, p), env=os.environ, shell=True) 
				if os.path.exists(p+".gpg"):
					os.lchown(p+".gpg",uid,gid)
				# CLEANUP LOOP START:
				# for every link to a tarball, make sure that links to hashes are in place.
				# also clean up dead tarball links:
	for link in links:
		# link = link to a TARBALL
		abs_realpath = os.path.normpath(os.path.join(os.path.dirname(link),os.readlink(link)))
		if os.path.exists(abs_realpath):
			dead = False
		else:
			# tarball link is stale, doesn't exist at all
			dead = True
			print(link + " dead; cleaning up")
		if dead:
			os.unlink(link)
		for ext in ['.hash.txt', '.gpg']:
			extlink = link + ext
			# if link to hash or gpg exists...
			if dead and os.path.lexists(extlink):
				# remove it!
				print(extlink + " dead; cleaning up")
				os.unlink(extlink)
			# if the real hash file exists...
			if not dead and os.path.exists(abs_realpath + ext):
				# create link to hash!
				if os.path.lexists(extlink):
					os.unlink(extlink)
				os.symlink(os.readlink(link) + ext, extlink)
			if os.path.lexists(extlink):
				# we found a link, let's make sure it has correct perms
				os.lchown(extlink, uid, gid)
		# CLEANUP LOOP START
	print()
	print("Cleaning up old hashes...")
	# This check goes the other way -- find all hashes, and make sure that the corresponding tarball exists. Otherwise,
	# remove.
	for root, dirnames, filenames in os.walk(initial_path):
		for ext in [ '.hash.txt', '.gpg' ]:
			for filename in fnmatch.filter(filenames, '*' + ext):
				p = os.path.join(root, filename)
				ptar = p[:-len(ext)]
				if not os.path.exists(ptar):
					os.unlink(p)
					sys.stdout.write('.')
					sys.stdout.flush()
	print()
	print("Done!")
else:
	print("Don't understand: %s" % sys.argv[1])
	sys.exit(1)

# vim: ts=4 sw=4 noet
